package com.theeviljames.coursework.ann;


import java.util.Vector;
import com.theeviljames.coursework.problems.*;
import com.theeviljames.coursework.exceptions.MatrixException;


public class ANN {

	private double learnRate;
	private double alpha;
	private ANNLayer[] ann;
	private double[][] deltaError;
	private Vector<Double> errorHistory = new Vector<Double>();
	
	public ANN(int[] topology, double learnRate, double alpha, boolean bias) {
		this.learnRate = learnRate;
		this.alpha = alpha;
		ann = new ANNLayer[topology.length-1];
		for(int i = 0; i < topology.length-1; i++){
			ann[i] = new ANNLayer(topology[i],topology[i+1],bias,(i==0));
		}
	}

	public double[][] getOutputs(double[][] inputs){
		double[][] outputs = ann[0].getOutputs(inputs);
		for(int i = 1; i < ann.length; i++) outputs = ann[i].getOutputs(outputs);
		return ANNMatrix.sigmoid(outputs);
	}
	
	public Vector<Double> getErrorHistory(){
		return errorHistory;
	}
	
	public void train(double[][][] trainingSet, double[][][] targetSet, int epochs, double maxError) throws MatrixException{
		long start = System.nanoTime();
		for(int i = 0; i < epochs; i++){
			double error = 0.0;
			for(int j = 0; j < trainingSet.length; j++){
				//O is the Output
				double[][] O = getOutputs(trainingSet[j]);
				//E is the error E = T - O
				double[][] E = ANNMatrix.subtract(targetSet[j], O);
				//E = ANNMatrix.elemTimes(E,E);
				//OPrime is g'(O2) which is f(O2)(1-f(O2)) = O*(1-O)
				double[][] OPrime = ANNMatrix.elemTimes(O, ANNMatrix.oneMinus(O));
				//D is Delta Error at the output layer which is O*(1-O)*E = OPrime*E
				double[][] D = ANNMatrix.elemTimes(OPrime, E);
				//Propagate the error backwards
				deltaError = D;
				backPropagateError(D);
				//Update the weights
				updateWeights();
				
				error += sumOfSquaredError(E);
			}
			errorHistory.add(error);
			if(error<maxError){
				System.out.println("Training complete in " + i + " epochs");break;
			}
		}
		System.out.println("Training took >"+(System.nanoTime()-start)/1000 + "microseconds");
	}
	
	public static double sumOfSquaredError(double[][] error) throws MatrixException{
		return ANNMatrix.times(ANNMatrix.transpose(error), error)[0][0];
	}
	
	public void backPropagateError(double[][] delta) throws MatrixException{
		
		double[][] O = ann[ann.length-1].f();
		//OPrime is same as above (in train())
		double[][] OPrime = ANNMatrix.elemTimes(O, ANNMatrix.oneMinus(O));
		//W is the weights
		double[][] W = ann[ann.length-1].getWeights();
		//WD is W*D
		double[][] WD = ANNMatrix.times(W, delta);
		//D is OPrime*WD
		double[][] D = ANNMatrix.elemTimes(OPrime, WD);
		ann[ann.length-1].setDeltaError(D);
		
		//For each layer
		for(int i = ann.length-2; i >= 0; i--){
			//O is the output of the nodes before it goes through the weights
			O = ann[i].f();
			//OPrime is same as above (in train())
			if(i!=0)OPrime = ANNMatrix.elemTimes(O, ANNMatrix.oneMinus(O));
			else OPrime = O;
			//W is the weights
			W = ann[i].getWeights();
			//WD is W*D
			WD = ANNMatrix.times(W, trim(ann[i+1].getDeltaError()));
			D = ANNMatrix.elemTimes(OPrime, WD);
			ann[i].setDeltaError(D);
		}
	}
	
	public void updateWeights() throws MatrixException{
		//WC is the weight change = R*O*D'
		double[][] O = ann[ann.length-1].f();
		double[][] OD = ANNMatrix.times(O, ANNMatrix.transpose(deltaError));
		double[][] WC = ANNMatrix.scalarTimes(OD, learnRate);
		WC = ANNMatrix.add(WC, ANNMatrix.scalarTimes(ann[ann.length-1].getWeightChange(), alpha));
		ann[ann.length-1].setWeightChange(WC);
		//W is the new weights
		double[][] W = ANNMatrix.add(ann[ann.length-1].getWeights(), WC);
		ann[ann.length-1].setWeights(W);
		
		for(int i = ann.length-2; i >=0; i--){
			O = ann[i].f();
			OD = ANNMatrix.times(O, ANNMatrix.transpose(trim(ann[i+1].getDeltaError())));
			WC = ANNMatrix.scalarTimes(OD, learnRate);
			WC = ANNMatrix.add(WC, ANNMatrix.scalarTimes(ann[i].getWeightChange(), alpha));
			ann[i].setWeightChange(WC);
			W = ANNMatrix.add(ann[i].getWeights(), WC);
			ann[i].setWeights(W);
		}
		
	}
	
	public void test(double[][][] validationSet, double[][][] targetSet){
		//For future problems would implement validation whilst training but
		//for the n-bit parity problems no point
	}
	
	public void setWeights(double[][][] weights){
		for(int i = 0; i < ann.length; i++){
			ann[i].setWeights(weights[i]);
		}
	}
	
	public void run(IANNProblem problem, int epochs, double maxError){
		try{
			train(problem.trainingSet(), problem.trainingTargetSet(),epochs, maxError);
			//test(problem.validationSet(), problem.validationTargetSet());
		}
		catch(Exception e){
			e.printStackTrace();
		}
		
	}
	
	public double[][] trim(double[][] a){
		double[][] b = new double[a.length-1][1];
		for(int i = 0; i < b.length; i++)b[i][0] = a[i][0];
		return b;
	}
}
